#include <stdio.h>
#include "assload.h"
#include "logger.h"
#include "datapath.h"

#ifdef USE_ASSIMP

#include <vector>
#include <map>
#include "assimp/cimport.h"
#include "assimp/scene.h"
#include "assimp/postprocess.h"
#include "texman.h"
#include "material.h"
#include "scene.h"

using namespace std;
using namespace goatgfx;

static bool load_material(Material *mat, const aiMaterial *aimat);
static Object *load_node(const aiScene *aiscn, const aiNode *ainode);
static Mesh *load_mesh(const aiScene *aiscn, const aiMesh *aimesh);
static Curve *load_curve(const aiScene *aiscn, const aiMesh *aimesh);
static bool load_bones(Mesh *mesh, const aiMesh *aimesh);

static Vector3 assimp_vector(const aiVector3D &v);
static Quaternion assimp_quat(const aiQuaternion &q);
static Matrix4x4 assimp_matrix(const aiMatrix4x4 &aim);
static long assimp_time(const aiAnimation *anim, double aitime);
static void print_hierarchy(const aiNode *node);

static map<string, Object*> obj_by_name;
static map<aiMesh*, Mesh*> mesh_by_aimesh;

bool load_ass(Scene *scn, const char *fname)
{
	static bool init_done;

	if(!init_done) {
		static aiLogStream log_stream = aiGetPredefinedLogStream(aiDefaultLogStream_STDOUT, 0);
		aiAttachLogStream(&log_stream);
		//aiEnableVerboseLogging(1);
		init_done = true;
	}

	unsigned int proc_flags = aiProcess_JoinIdenticalVertices |
		aiProcess_CalcTangentSpace |
		aiProcess_Triangulate |
		aiProcess_SortByPType |
		aiProcess_FlipUVs;

	const aiScene *aiscn = aiImportFile(datafile_path(fname).c_str(), proc_flags);
	if(!aiscn) {
		error_log("failed to load file: %s\n", fname);
		return false;
	}

	info_log("NODE HIERARCHY:\n");
	print_hierarchy(aiscn->mRootNode);
	info_log("-------------------\n");

	Vector3 root_pos, root_scaling(1.0, 1.0, 1.0);
	Quaternion root_rot;

	if(aiscn->mRootNode) {
		Matrix4x4 root_matrix = assimp_matrix(aiscn->mRootNode->mTransformation);
		root_pos = root_matrix.get_translation();
		root_rot = root_matrix.get_rotation_quat();
		root_scaling = root_matrix.get_scaling();
	}

	// load all meshes
	for(unsigned int i=0; i<aiscn->mNumMeshes; i++) {
		aiMesh *aimesh = aiscn->mMeshes[i];
		Mesh *mesh;
		Curve *curve;

		switch(aimesh->mPrimitiveTypes) {
		case aiPrimitiveType_TRIANGLE:
			if((mesh = load_mesh(aiscn, aimesh))) {
				mesh_by_aimesh[aimesh] = mesh;
				scn->meshes.push_back(mesh);
			}
			break;

		case aiPrimitiveType_LINE:
			if((curve = load_curve(aiscn, aimesh))) {
				scn->curves.push_back(curve);
			}
			break;

		default:
			error_log("unsupported primitive type: %u\n", aimesh->mPrimitiveTypes);
			break;
		}
	}

	// load all the nodes recursively
	for(unsigned int i=0; i<aiscn->mRootNode->mNumChildren; i++) {
		Object *obj = load_node(aiscn, aiscn->mRootNode->mChildren[i]);
		if(obj) {
			Object *dummy = new Object;
			dummy->set_name((string("dummyroot_") + string(obj->get_name())).c_str());
			dummy->set_position(root_pos);
			dummy->set_rotation(root_rot);
			dummy->set_scaling(root_scaling);
			dummy->add_child(obj);

			obj = dummy;
			scn->objects.push_back(obj);
		}
	}

	// load and attach the bones to the meshes
	for(unsigned int i=0; i<aiscn->mNumMeshes; i++) {
		aiMesh *aimesh = aiscn->mMeshes[i];

		Mesh *mesh = mesh_by_aimesh[aimesh];
		load_bones(mesh, aimesh);
	}

	obj_by_name.clear();
	mesh_by_aimesh.clear();

	aiReleaseImport(aiscn);
	return true;
}

static bool load_material(Material *mat, const aiMaterial *aimat)
{
	aiColor4D aicol;
	float shin, shin_str;

	if(aiGetMaterialColor(aimat, AI_MATKEY_COLOR_DIFFUSE, &aicol) == 0) {
		mat->diffuse = Vector3(aicol[0], aicol[1], aicol[2]);
	}
	if(aiGetMaterialColor(aimat, AI_MATKEY_COLOR_SPECULAR, &aicol) == 0) {
		mat->specular = Vector3(aicol[0], aicol[1], aicol[2]);
	}

	unsigned int count = 1;
	if(aiGetMaterialFloatArray(aimat, AI_MATKEY_SHININESS_STRENGTH, &shin_str, &count) != 0) {
		shin_str = 1.0;
	}
	if(aiGetMaterialFloatArray(aimat, AI_MATKEY_SHININESS, &shin, &count) == 0) {
		// XXX can't remember how I came up with this...
		mat->shininess = shin * shin_str * 0.0001 * 128.0;
	}

	// load textures
	struct { int type; aiTextureType aitype; } textypes[] = {
		{TEX_DIFFUSE, aiTextureType_DIFFUSE},
		{TEX_NORMAL, aiTextureType_NORMALS},
		{TEX_SPECULAR, aiTextureType_SPECULAR}
	};

	for(int i=0; i<sizeof textypes / sizeof *textypes; i++) {
		aiString aipath;

		if(aiGetMaterialTexture(aimat, textypes[i].aitype, 0, &aipath) == 0) {
			char *tmp, *fname = aipath.data;

			if((tmp = strrchr(fname, '/'))) {
				fname = tmp + 1;
			}
			if((tmp = strrchr(fname, '\\'))) {
				fname = tmp + 1;
			}

			if(*fname) {
				mat->tex[textypes[i].type] = texset.get(fname);
			}
		}
	}

	return true;
}

static Object *load_node(const aiScene *aiscn, const aiNode *ainode)
{
	Object *obj = new Object;
	obj->set_name(ainode->mName.data);

	if(ainode->mNumMeshes) {
		if(ainode->mNumMeshes > 1) {
			info_log("%s warning: node %s has more than one meshes (%u)\n", __FUNCTION__,
					ainode->mName.data, ainode->mNumMeshes);
		}

		aiMesh *aimesh = aiscn->mMeshes[ainode->mMeshes[0]];
		obj->set_mesh(mesh_by_aimesh[aimesh]);

		// also grab the material of this mesh
		load_material(&obj->material, aiscn->mMaterials[aimesh->mMaterialIndex]);
	}

	// if there are animations, grab the first and try to use it
	if(aiscn->mNumAnimations) {
		aiAnimation *aianim = aiscn->mAnimations[0];
		aiNodeAnim *ainodeanim = 0;
		for(unsigned int i=0; i<aianim->mNumChannels; i++) {
			if(strcmp(aianim->mChannels[i]->mNodeName.data, ainode->mName.data) == 0) {
				ainodeanim = aianim->mChannels[i];
				break;
			}
		}

		if(ainodeanim) {
			// load all position (translation) keyframes
			for(unsigned int i=0; i<ainodeanim->mNumPositionKeys; i++) {
				Vector3 pos = assimp_vector(ainodeanim->mPositionKeys[i].mValue);
				long msec = assimp_time(aianim, ainodeanim->mPositionKeys[i].mTime);
				obj->set_position(pos, msec);
			}

			// load all rotation keyframes
			for(unsigned int i=0; i<ainodeanim->mNumRotationKeys; i++) {
				Quaternion rot = assimp_quat(ainodeanim->mRotationKeys[i].mValue);
				if(rot.length_sq() < SMALL_NUMBER) {
					continue;
				}
				rot.normalize();
				long msec = assimp_time(aianim, ainodeanim->mRotationKeys[i].mTime);
				obj->set_rotation(rot, msec);
			}

			// load all scaling keyframes
			for(unsigned int i=0; i<ainodeanim->mNumScalingKeys; i++) {
				Vector3 scale = assimp_vector(ainodeanim->mScalingKeys[i].mValue);
				long msec = assimp_time(aianim, ainodeanim->mScalingKeys[i].mTime);
				obj->set_scaling(scale, msec);
			}

			obj->set_extrapolator(EXTRAP_REPEAT);	// loop animation
		} else {
			Matrix4x4 local_matrix = assimp_matrix(ainode->mTransformation);
			obj->set_local_matrix(local_matrix);
		}
	}

	/* recurse to all children */
	for(unsigned int i=0; i<ainode->mNumChildren; i++) {
		Object *child = load_node(aiscn, ainode->mChildren[i]);
		if(child) {
			obj->add_child(child);
		}
	}

	obj_by_name[obj->get_name()] = obj;
	return obj;
}

static Mesh *load_mesh(const aiScene *aiscn, const aiMesh *aimesh)
{
	Mesh *mesh = new Mesh;

	int num_verts = aimesh->mNumVertices;
	int num_faces = aimesh->mNumFaces;

	mesh->set_attrib_data(MESH_ATTR_VERTEX, 3, num_verts, (float*)aimesh->mVertices);

	if(aimesh->mNormals) {
		mesh->set_attrib_data(MESH_ATTR_NORMAL, 3, num_verts, (float*)aimesh->mNormals);
	}
	if(aimesh->mTangents) {
		mesh->set_attrib_data(MESH_ATTR_TANGENT, 3, num_verts, (float*)aimesh->mTangents);
	}
	if(aimesh->mTextureCoords[0]) {
		mesh->set_attrib_data(MESH_ATTR_TEXCOORD, 3, num_verts, (float*)aimesh->mTextureCoords[0]);
	}

	if(aimesh->mBones) {
		float *weights = mesh->set_attrib_data(MESH_ATTR_BONEWEIGHTS, 4, num_verts, 0);
		float *boneidx = mesh->set_attrib_data(MESH_ATTR_BONEIDX, 4, num_verts, 0);

		memset(weights, 0, num_verts * 4 * sizeof *weights);
		memset(boneidx, 0, num_verts * 4 * sizeof *boneidx);

		int *vertex_bone_count = new int[num_verts];
		memset(vertex_bone_count, 0, num_verts * sizeof *vertex_bone_count);

		for(unsigned int i=0; i<aimesh->mNumBones; i++) {
			aiBone *aibone = aimesh->mBones[i];

			// for every vertex affected by this bone:
			for(unsigned int j=0; j<aibone->mNumWeights; j++) {
				aiVertexWeight *aiweight = aibone->mWeights + j;
				int vidx = aiweight->mVertexId;
				int vert_boneidx = vertex_bone_count[vidx];
				if(vert_boneidx >= 4) {
					error_log("WARNING vertex with more than 4 bones found\n");
					continue;
				}

				weights[vidx * 4 + vert_boneidx] = aiweight->mWeight;
				boneidx[vidx * 4 + vert_boneidx] = (float)i;
				vertex_bone_count[vidx]++;
			}
		}

		delete [] vertex_bone_count;

		// normalize weights
		for(int i=0; i<num_verts; i++) {

			float wsum = 0.0f;

			for(int j=0; j<4; j++) {
				wsum += weights[i * 4 + j];
			}

			if(1.0 - wsum > 1e-4) {
				error_log("WARNING vertex with weights < 1 (%f), normalizing...\n", wsum);

				if(wsum < 1e-6) {
					// this is clearly broken, let's use the first bone in full
					weights[i * 4] = 1.0;
				} else {
					weights[i * 4] /= wsum;
					weights[i * 4 + 1] /= wsum;
					weights[i * 4 + 2] /= wsum;
					weights[i * 4 + 3] /= wsum;
				}
			}
		}
	}

	unsigned int *iptr = mesh->set_index_data(num_faces * 3);
	for(int i=0; i<num_faces; i++) {
		for(int j=0; j<3; j++) {
			*iptr++ = aimesh->mFaces[i].mIndices[j];
		}
	}

	return mesh;
}

static Curve *load_curve(const aiScene *aiscn, const aiMesh *aimesh)
{
	Curve *curve = new Curve;

	for(unsigned int i=0; i<aimesh->mNumVertices; i++) {
		Vector3 pt = assimp_vector(aimesh->mVertices[i]);
		curve->add_point(pt);
	}
	info_log("loaded curve with %d points\n", aimesh->mNumVertices);

	return curve;
}

static bool load_bones(Mesh *mesh, const aiMesh *aimesh)
{
	if(!aimesh->mNumBones) {
		return false;
	}

	for(unsigned int i=0; i<aimesh->mNumBones; i++) {
		aiBone *aibone = aimesh->mBones[i];
		Object *obj = obj_by_name[aibone->mName.data];
		if(!obj) {
			error_log("bone %s not found\n", aibone->mName.data);
			continue;
		}

		obj->set_bone_matrix(assimp_matrix(aibone->mOffsetMatrix));
		mesh->add_bone(obj);

		info_log("adding bone: %s\n", obj->get_name());
	}

	return true;
}

static Vector3 assimp_vector(const aiVector3D &v)
{
	return Vector3(v[0], v[1], v[2]);
}

static Quaternion assimp_quat(const aiQuaternion &q)
{
	return Quaternion(q.w, Vector3(q.x, q.y, q.z));
}

static Matrix4x4 assimp_matrix(const aiMatrix4x4 &aim)
{
	Matrix4x4 m;
	memcpy(m[0], &aim, 16 * sizeof(float));
	return m;
}

/* convert an assimp keyframe time (ticks) into milliseconds */
static long assimp_time(const aiAnimation *anim, double aitime)
{
	double sec;
	if(anim->mTicksPerSecond < 1e-6) {
		// assume time is in frames?
		sec = aitime / 30.0;
	} else {
		sec = aitime / anim->mTicksPerSecond;
	}
	return (long)(sec * 1000.0);
}

static void print_hierarchy(const aiNode *node)
{
	static int lvl;
	static int lvlopen[256];

	for(int i=0; i<lvl; i++) {
		putchar(' ');
		if(lvlopen[i]) {
			putchar(i >= lvl - 1 ? '+' : '|');
		} else {
			putchar(i >= lvl - 1 ? '+' : ' ');
		}
	}
	info_log("- \"%s\"\n", node->mName.data);

	lvlopen[lvl] = 1;

	lvl++;
	for(unsigned int i=0; i<node->mNumChildren; i++) {
		if(i == node->mNumChildren - 1) {
			lvlopen[lvl - 1] = 0;
		}
		print_hierarchy(node->mChildren[i]);
	}
	lvl--;
}

#else	// !defined USE_ASSIMP

using namespace goatgfx;

bool load_ass(Scene *scn, const char *fname)
{
	error_log("load_ass: assimp support not compiled in\n");
	return false;
}

#endif
